Buffer Pool配置调优
- Buffer Pool 太小会频繁刷盘,太大又挤压其他进程内存,一般给机器内存的 50%~60%
- 多实例能分散锁竞争,但前提是总内存 >= 1G,每个实例建议 1G 左右
- chunk 是内存分配的基本单位(默认 128M),让动态扩缩容不用申请连续大块内存
- size 必须是 chunk_size × instances 的整数倍,不然 MySQL 会自动向上调整
1. Buffer Pool 是干嘛的?
简单说就是 InnoDB 的数据缓存区。你查数据、改数据,都是先把磁盘上的页(page)加载到 Buffer Pool 里,然后在内存里操作。操作完不一定马上写回磁盘,有个后台线程定期刷盘。
所以 Buffer Pool 越大,能缓存的数据越多,磁盘 IO 就越少,性能自然好。但也不能太大,得给操作系统、其他进程、连接栈内存等留够空间。系统层面的内存参数可以参考内核参数调优。
Buffer Pool 的内部结构(三套链表、LRU 冷热分离等)详见 02-BufferPool与内存管理。
2. 为什么要搞多实例?
单个 Buffer Pool 实例有个问题:所有线程访问都得抢同一把锁。并发量一上来,锁竞争就成了瓶颈。
这把锁就是 buf_pool->mutex(互斥锁),它保护以下共享数据结构的并发访问:
| 受保护的操作 | 说明 |
|---|---|
| free 链表 | 从空闲链表取出/归还缓存页 |
| lru 链表 | 插入新页、提升热数据、淘汰尾部页 |
| flush 链表 | 追踪脏页、刷盘时移除脏页 |
| page hash | 查找页是否已在缓存中 |
竞争场景举例:线程 A 要查 user 表,需要加锁查 page hash → 没命中 → 加锁从 free 链表取空闲页 → 加锁插入 lru 链表。整个过程都要持有同一把 buf_pool->mutex。
多实例的解法就是:把一个大 Buffer Pool 拆成 N 个实例,每个实例独立管理自己的内存块和链表,有自己的锁。线程根据 (space_id, page_no) % 实例数 分散到不同实例上,竞争就小了。
[server]
innodb_buffer_pool_size = 8G
innodb_buffer_pool_instances = 4
这段配置意思是:总共 8G 内存,分成 4 个实例,每个实例 2G。
注意:有个硬性门槛——总内存小于 1G 时,不管你设多少 instances,MySQL 只会给你一个实例。
3. chunk 是怎么回事?
Buffer Pool 的内存结构分两层:
Buffer Pool(总内存,如 8G)
├── 实例1(如 2G)← innodb_buffer_pool_instances
│ ├── chunk1(128M)← innodb_buffer_pool_chunk_size
│ ├── chunk2(128M)
│ ├── ...(共 16 个)
│ └── 自己的 mutex + free/flush/lru 链表
├── 实例2(2G)
│ ├── chunk1 ~ chunk16
│ └── 自己的 mutex + free/flush/lru 链表
├── 实例3(2G)...
└── 实例4(2G)...
- 实例:锁和链表的独立管理单元,目的是分散锁竞争
- chunk:实例内部的内存分配单元,目的是扩缩容灵活
chunk 大小通过 innodb_buffer_pool_chunk_size 控制,默认 128M。
分 chunk 的好处:比如从 8G 扩到 16G:
- 传统做法:申请 16G 连续内存,把旧数据拷过去,释放旧内存。16G 连续内存不好申请,还可能 OOM。
- chunk 做法:申请 64 个 128M 的小 chunk,分给 4 个实例,每个实例加 16 个 chunk。128M 连续内存很容易申请到。
graph LR
subgraph 扩容前["8G(4实例 × 2G)"]
E1["实例1
chunk×16"]
E2["实例2
chunk×16"]
E3["实例3
chunk×16"]
E4["实例4
chunk×16"]
end
N["申请 64 个 128M chunk"]
subgraph 扩容后["16G(4实例 × 4G)"]
E1_new["实例1
chunk×32"]
E2_new["实例2
chunk×32"]
E3_new["实例3
chunk×32"]
E4_new["实例4
chunk×32"]
end
扩容前 --> N --> 扩容后4. 生产环境怎么配?
经验值:Buffer Pool 占机器内存的 50%~60%。
配置约束:innodb_buffer_pool_size 必须是 chunk_size × instances 的整数倍,不然 MySQL 会自动向上取整,你设 7.5G 它可能实际给了 8G,容易踩坑。
以 12G 机器、分配 8G 为例:
每个实例 1G 左右 → instances = 8
chunk_size = 128M(默认)
分配单元 = 128M × 8 = 1G
8G ÷ 1G = 8,整除 ✓
[mysqld]
innodb_buffer_pool_size = 8G
innodb_buffer_pool_instances = 8
# chunk_size 默认 128M,一般不用动
经验速查:8G 内存配 8 个实例,16G 配 16 个,别超过 64 个。
更多 MySQL 开发和调优经验可以看 MySQL开发规范清单,那里整理了建表、索引、SQL 写法等方面的规范。
5. 配完怎么验证?
重启 MySQL 后可以用这些命令看看实际分配情况:
SHOW STATUS LIKE 'Innodb_buffer_pool%';
重点关注:
Innodb_buffer_pool_pages_total:总页数,乘以 16K 就是实际分配的内存Innodb_buffer_pool_pages_free:空闲页数,如果长期很少说明 Buffer Pool 偏小Innodb_buffer_pool_read_requests:逻辑读次数Innodb_buffer_pool_reads:物理读次数(没命中缓存,要去磁盘读)
物理读 / 逻辑读 = 缓存未命中率,越低越好,一般要控制在 1% 以内。
Buffer Pool 调好后,连接池那边也要配合优化,让连接数和实例数匹配,减少跨实例竞争。具体可以看 MySQL的JDBC与连接池调优#4 连接池与 Buffer Pool 实例数匹配。